from io import TextIOBase
import subprocess
import threading
import time
try:
    from queue import Queue, Empty
except ImportError:
    from Queue import Queue, Empty  # python 2.x

def singleton(cls):
    instances = dict({})
    def single(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return single

class Utils:
    def getTimeStamp():
        return int(time.time())

class TerminalAsyncExe:
    def __init__(self, process, cb) -> None:
        self.thread = None
        self.process = process
        self.cb = cb
        self.id = 0

@singleton
class Terminal:
    def __init__(self) -> None:
        self.asyncExe = list()
    
    def run(self, cmd):
        ret = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).stdout.read().decode("utf-8")
        print("Terminal::run ----> %s:%s" % (cmd, ret))
        return ret

    def runAsync(self, cmd, cb):
        ret = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
        exe = TerminalAsyncExe(ret, cb)
        exe.id = len(self.asyncExe)
        for i in range(0, len(self.asyncExe), 1):
            if self.asyncExe[i] == None:
                exe.id = i
                self.asyncExe[i] = exe
                break
        if exe.id >= len(self.asyncExe):
            self.asyncExe.append(exe)
        exe.thread = threading.Thread(target=self.outputExeThread, args=[exe])
        exe.thread.start()
        print("Terminal::runAsync ----> %s:%d" % (cmd, exe.id))
        return exe.id
    
    def killAsync(self, id):
        if id >= 0 and len(self.asyncExe) > id and self.asyncExe[id] != None:
            self._killAsync(self.asyncExe[id])
    
    def _killAsync(self, exe, forceClose = True):
        self.asyncExe[exe.id] = None
        if forceClose:
            exe.process.terminate()
            exe.process.stdout.close()
        print("Terminal::_killAsync %d" % (exe.id))
    
    def outputExeThread(self, exe):
        print("enter thread")
        while True:
            line = exe.process.stdout.readline()
            if not line:
                break
            print("Terminal::runAsync ----> output %s" % (line))
            exe.cb(line)
        exe.process.stdout.close()
        self._killAsync(exe, False)

class TickCenterParam:
    def __init__(self, interval, count = -1) -> None:
        self.interval = interval
        self.count = count
        self.tick = 0

class EventStub:
    def __init__(self, target, func) -> None:
        self.target = target
        self.func = func
        self.removed = False

@singleton
class EventCenter:
    def __init__(self) -> None:
        self.stubsByMsg = dict({})
        self.callDepth = 0
    def subscribe(self, event, target, func):
        container = self.stubsByMsg.get(event, None)
        if container == None:
            container = list()
            self.stubsByMsg[event] = container
        targetStub = None
        for item in container:
            if item.target == target and item.func == func: #already contains it
                item.removed = False
                targetStub = item
        if targetStub == None:
            targetStub = EventStub(target, func)
            container.append(targetStub)
    def unsubscribe(self, event, target, func = None):
        container = self.stubsByMsg.get(event, None)
        if container != None:
            for item in container:
                if item.target == target and (func == None or item.func == func):
                    item.removed = True
        self._checkRemovedStub()
    def trigger(self, event, *args, **kwArgs):
        self.callDepth += 1
        container = self.stubsByMsg.get(event, None)
        if container != None:
            for item in container:
                if not item.removed:
                    if item.target != None:
                        item.func(item.target, *args, **kwArgs)
                    else:
                        item.func(*args, **kwArgs)
        self.callDepth -= 1
        if self.callDepth <= 0:
            self._checkRemovedStub()
    def _checkRemovedStub(self):
        for container in self.stubsByMsg.values():
            start = len(container) - 1
            for i in range(start, -1, -1):
                if container[i].removed:
                    del container[i]

@singleton
class TickCenter:
    def __init__(self) -> None:
        self.tickers = dict({})
        self.tickersToRemove = list()
    #param t should have tick(dt) method
    def schedule(self, t, interval):
        self.tickers[t] = TickCenterParam(interval)
    def scheduleOnce(self, t, interval):
        self.tickers[t] = TickCenterParam(interval, count=1)
    def unschedule(self, t):
        if t in self.tickers:
            del self.tickers[t]
    def tick(self, dt):
        for k, ticker in self.tickers.items():
            ticker.tick += dt
            if ticker.tick >= ticker.interval:
                k.tick(ticker.tick)
                ticker.tick = 0
                if ticker.count > 0:
                    ticker.count -= 1
                    if ticker.count <= 0:
                        self.tickersToRemove.append(k)
        if len(self.tickersToRemove) > 0:
            for ticker in self.tickersToRemove:
                del self.tickers[ticker]
            self.tickersToRemove.clear()

